# encoding: utf-8
#

require 'redmine/scm/adapters/abstract_adapter'
require 'rexml/document'

module Redmine
  module Scm
    module Adapters
      class PlasticAdapter < AbstractAdapter

        class PlasticBranch < Branch 
          attr_accessor :is_default
        end
        
        # Plastic executable name
        CM_BIN = "cm"
        
                
        class << self
          def client_command
            @@bin    ||= CM_BIN
          end

          def sq_bin
            @@sq_bin ||= shell_quote_command
          end

          def client_version
            @@client_version ||= (scm_command_version || [])
          end

          def client_available
            !client_version.empty?
          end

          def scm_command_version
            scm_version = scm_version_from_command_line.dup
            if scm_version.respond_to?(:force_encoding)
              scm_version.force_encoding('ASCII-8BIT')
            end
            #scm_version.is_a?(Array) ? scm_version.join('.') : scm_version.to_s.strip
            if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
              m[2].scan(%r{\d+}).collect(&:to_i)
            end            
          end

          def scm_version_from_command_line
            shellout("#{sq_bin} version") { |io| io.read }.to_s
          end
        end

        def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
          super
          logger.debug { "plastic initialize url:#{url}, root_url:#{root_url}" }
          #@root_url = root_url.blank? ? 'localhost:8087' : root_url
          @path_encoding = path_encoding.nil? || path_encoding.empty? ? 'UTF-8' : path_encoding
        end

        def branches
          return @branches if @branches
          @branches = []
          cmd = %Q{#{self.class.sq_bin} find branch "on repository '#{url}'" --nototal --format="{name} {id} {changeset}\"}
          default = default_branch
          shellout(cmd) do |io|
            io.each_line do |line|
              name, id, changeset = line.split
              logger.debug { "#{name}, #{id},#{changeset} " }
              bran = PlasticBranch.new(name)
              bran.revision = changeset
              bran.scmid = changeset
              bran.is_default = name == default
              @branches << bran
            end
          end
          @branches.sort!
        rescue ScmCommandAborted
          nil
        end

        def default_branch
          "/main"
        end

        def tags
          return @tags if @tags
          cmd = %Q{#{self.class.sq_bin} find label "on repository '#{url}'" --nototal --format="{name}"}
          shellout(cmd) do |io|
            @tags = io.readlines.sort!.map{|t| t.strip}
          end
        rescue ScmCommandAborted
          nil
        end
        
        #
        #
        #
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
          logger.debug {"<plastic> revisions path=#{path}, from=#{identifier_from}, to=#{identifier_to}, options=#{options.inspect}"}
          revs = Revisions.new
          brs = get_branches_on_revisions(identifier_from, identifier_to, options)          
          # brs --> changeset --> revisions, moved, removed, 
          brs.each do |br,value|
            cmd = get_cmd_find_changeset(br, value)
            cm_find(cmd) do |io|
              begin                
                doc = REXML::Document.new(io)
                if doc.root.name == 'PLASTICQUERY'
                  doc.elements.each("PLASTICQUERY/CHANGESET") do |changeset|
                    logger.debug "<plastic> revisions changeset:#{changeset}"
                    changesetid = changeset.elements['CHANGESETID'].text                    
                    fils = []
                    files = update_files(changesetid, files)
                    logger.debug "<plastic> revisions file #{changesetid} ... #{files}"
                    attributes = get_attribute_on_revision(changeset, changesetid, files)
                    revision = Revision.new(attributes)
                    if block_given?
                      yield revision
                    else
                      revs << revision
                    end
                  end
                end                   
              rescue
              end              
            end            
          end
          revs
        rescue ScmCommandAborted => e
          err_msg = "plastic revisions error: #{e.message}"
          logger.error(err_msg)
          if block_given?
            raise CommandFailed, err_msg
          else
            revs
          end          
        end

        def get_cmd_find_changeset(br, value)
          cmd = %w|changeset|
          cmd <<  %Q{where "branch='#{br.to_s}'"}            
          #cmd = %Q{#{self.class.sq_bin} find changeset where branch=#{br.to_s} "on repository '#{url}'"}
          unless value[:rev_to].nil?
            cmd << "and"
            cmd << %Q{"changesetid <= #{value[:rev_to]}"}
          end
          unless value[:rev_from].nil?
            cmd << "and"
            cmd << %Q{"changesetid > #{value[:rev_from]}"}
          end
          cmd << %Q{"on repository '#{url}'"}             
          cmd << "--xml"
          cmd
        end
        
        def get_branches_on_revisions(identifier_from=nil, identifier_to=nil, options={})
          brs ||= {}
          if identifier_from || identifier_to
          else
            #scm_brs = branches
            #scm_brs.each_with_index do |br,i|
            #br_hash = { :name => br }
            unless (options[:includes].nil? || options[:includes].empty?) #.blank?
              include_brs = branches_with_key(options[:includes],:rev_to)
              logger.debug "brs #{include_brs}"
              brs.merge!(include_brs)
            end
            unless options[:excludes].nil? || options[:excludes].empty? #.blank?
              excludes_brs = branches_with_key(options[:excludes], :rev_from)
              logger.debug "brs #{include_brs}"
              brs.merge!(include_brs)
            end            
          end
          brs
        end
        
        # array : changset array
        def branches_with_key(array, key, h = nil)
          brs = h || {} 
          array.each do |scmid| 
            cmd = %Q{#{self.class.sq_bin} find changeset where "changesetid=#{scmid}" "on repository '#{url}'" --nototal --format="{branch} {changesetid}"}
            shellout(cmd) do |io|
            io.each_line do |line|
              name, changeset = line.split(' ')
              name_to_sym = name.to_sym
              logger.debug { "#{name}, #{changeset} " }
              unless brs.has_key?(name_to_sym)
                brs [name_to_sym]={}
              end
              brs [name_to_sym][key]=changeset              
              end
            end
          end
          return brs
        rescue ScmCommandAborted
          nil          
        end
        
        def update_files(csid, files = nil)
          result = files || []
          result = update_files_for_revison(csid, result)
          result = update_files_for_moved(csid, result) unless root_url.nil?                    
          result = update_files_for_removed(csid, result) unless root_url.nil?
          result
        end
        
        def update_files_for_revison(csid, files = nil)
          result = files || []
          ## find revision
          cmd_revs = %w|revision|
          cmd_revs << "where changeset=#{csid}"
          cmd_revs << %Q{"on repository '#{url}'"}
          cmd_revs << "--xml"
          cm_find(cmd_revs) do |io|
            begin
              doc = REXML::Document.new(io)
              if doc.root.name == 'PLASTICQUERY'
                doc.elements.each("PLASTICQUERY/REVISION") do |rev|
                  logger.debug "<plastic> update_files_for_revison ... #{rev}"
                  filepath = rev.elements['PATH'].text
                  fileaction = (rev.elements['PARENT'].text == "-1") ? "A" : "C"
                  result << {:action=>fileaction, :path=> path}
                end
              end
            rescue
            end
          end          
          result          
        end

        def update_files_for_moved(csid, files = nil)
          result = files || []
          result
        end
        
        def update_files_for_removed(csid, files = nil)
          result = files || []
          result
        end
        
        def get_attribute_on_revision(changeset, changesetid, files)
          attribute = {
            :identifier => changesetid,
            :scmid => changeset.elements['ID'].text,
            :name => changesetid,
            :author => changeset.elements['OWNER'].text,
            :time => Time.parse(changeset['DATE'].text),
            :message => changeset.elements['COMMENT'].text,
            :paths => files,
            :branch => changeset.elements['BRANCH'].text,
            :parents => changeset.elements['PARENT'].text.to_a
          }
          return attribute  
        end
        
        private
        
        def cm_find(args, options = {}, &block)
          cm_command("find", args, options, &block)
        end
        
        def cm_command(command, args, options = {}, &block)
          full_args = []
          full_args += args
          ret = shellout(
                   self.class.sq_bin + " #{command} " + full_args.map { |e| e.to_s }.join(' '),
                   options,
                   &block
                   )
          if $? && $?.exitstatus != 0
            raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
          end
          ret
        end
        
        # Helper that iterates over the child elements of a xml node
        # MiniXml returns a hash when a single child is found
        # or an array of hashes for multiple children
        def each_xml_element(node, name)
          if node && node[name]
            if node[name].is_a?(Hash)
              yield node[name]
            else
              node[name].each do |element|
                yield element
              end
            end
          end          
        end
        
      end
    end
  end
end